/** Copyright (c) 2014 Delcyon, Inc. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package com.delcyon.capo.resourcemanager.types; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PipedInputStream; import java.io.PipedOutputStream; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.EnumSet; import java.util.List; import java.util.Vector; import java.util.logging.Level; import javax.jcr.Binary; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.Property; import javax.jcr.PropertyIterator; import javax.jcr.RepositoryException; import javax.jcr.Value; import javax.jcr.version.Version; import javax.jcr.version.VersionHistory; import javax.jcr.version.VersionIterator; import javax.jcr.version.VersionManager; import org.apache.jackrabbit.core.ItemImpl; import org.apache.jackrabbit.core.ItemValidator; import org.apache.jackrabbit.core.NodeImpl; import org.apache.jackrabbit.core.SessionImpl; import org.apache.jackrabbit.core.session.SessionContext; import com.delcyon.capo.CapoApplication; import com.delcyon.capo.ContextThread; import com.delcyon.capo.datastream.stream_attribute_filter.ContentFormatTypeFilterInputStream; import com.delcyon.capo.datastream.stream_attribute_filter.MD5FilterInputStream; import com.delcyon.capo.datastream.stream_attribute_filter.MimeTypeFilterInputStream; import com.delcyon.capo.datastream.stream_attribute_filter.SizeFilterInputStream; import com.delcyon.capo.resourcemanager.ContentFormatType; import com.delcyon.capo.resourcemanager.ResourceParameter; import com.delcyon.capo.resourcemanager.ResourceParameterBuilder; import com.delcyon.capo.resourcemanager.ResourceURI; import com.delcyon.capo.resourcemanager.types.FileResourceType.Parameters; import com.delcyon.capo.server.jackrabbit.CapoJcrServer; import com.delcyon.capo.xml.cdom.VariableContainer; import com.delcyon.capo.xml.dom.ResourceDeclarationElement; /** * @author jeremiah * */ public class JcrResourceDescriptor extends AbstractResourceDescriptor implements Versionable { //private Session session; //private Node node; private String absPath = null; private JcrContentMetaData jcrResourceMetatData = null; //private boolean isLocalSession = false; private volatile Boolean isWriting = false; private Boolean hasPipeThreadStarted = false; private PipedOutputStream pipedOutputStream; private ItemValidator originalItemValidator; @Override public void init(ResourceDeclarationElement declaringResourceElement,VariableContainer variableContainer, LifeCycle lifeCycle,boolean iterate, ResourceParameter... resourceParameters) throws Exception { String parentDirParameter = getVarValue(variableContainer, Parameters.PARENT_PROVIDED_DIRECTORY); //String rootDirParameter = getVarValue(variableContainer, Parameters.ROOT_DIR); if (parentDirParameter != null && parentDirParameter.isEmpty() == false) { //append parent path to child this.absPath = ((JcrResourceDescriptor)CapoApplication.getDataManager().getResourceDirectory(parentDirParameter)).getResourceURI().getPath()+"/"+getResourceURI().getPath(); setResourceURI(new ResourceURI(getResourceType().getName()+":"+this.absPath)); } else { this.absPath = getResourceURI().getPath(); } super.init(declaringResourceElement, variableContainer, lifeCycle, iterate, resourceParameters); } @Override public void open(VariableContainer variableContainer,ResourceParameter... resourceParameters) throws Exception { advanceState(State.INITIALIZED, variableContainer, resourceParameters); if (getResourceState().ordinal() < State.OPEN.ordinal()) { // this.isLocalSession = false; // // if(Thread.currentThread() instanceof ContextThread) // { // this.session = ((ContextThread)Thread.currentThread()).getSession(); // } // else if(WApplication.getInstance() != null) // { // this.session = ((CapoWebApplication)WApplication.getInstance()).getJcrSession(); // } // else if(CapoJcrServer.getRepository() != null && getLifeCycle() == LifeCycle.EXPLICIT) // { // this.session = CapoJcrServer.getRepository().login(new SimpleCredentials("admin","admin".toCharArray())); // this.isLocalSession = true; // } // if(this.session == null) // { // if(CapoApplication.isServer()) //one last ditch effort // { // this.session = CapoJcrServer.getApplicationSession(); // } // if(this.session == null) // { // throw new Exception("Can't use JCR resources without a Session"); // } // } // CapoApplication.logger.log(Level.FINE, "JCRAssoc T="+Thread.currentThread()+" S="+CapoJcrServer.getSession()+" N="+absPath); // if(session.nodeExists(absPath) == true) // { // this.node = session.getNode(absPath); // } super.open(variableContainer,resourceParameters); // ((JcrContentMetaData) getResourceMetaData(variableContainer, resourceParameters)).setNode(this.node); } } @Override public StreamType[] getSupportedStreamTypes() throws Exception { return EnumSet.of(StreamType.INPUT, StreamType.OUTPUT).toArray(new StreamType[]{}); } @Override public StreamFormat[] getSupportedStreamFormats(StreamType streamType) throws Exception { if(streamType == StreamType.INPUT || streamType == StreamType.OUTPUT) { return EnumSet.of(StreamFormat.XML_BLOCK,StreamFormat.STREAM).toArray(new StreamFormat[]{}); } else { return null; } } @Override public boolean next(VariableContainer variableContainer,ResourceParameter... resourceParameters) throws Exception { advanceState(State.OPEN, variableContainer, resourceParameters); if(getResourceState() == State.OPEN) { // contentMetaData = new SimpleContentMetaData(getResourceURI()); // URL url = new URL(getResourceURI().getBaseURI()); // InputStream inputStream = contentMetaData.wrapInputStream(url.openConnection().getInputStream()); // content = contentMetaData.readInputStream(inputStream,true); setResourceState(State.STEPPING); return true; } else if (getResourceState() == State.STEPPING) { setResourceState(State.OPEN); return false; } return false; } @Override public ContentMetaData getContentMetaData(VariableContainer variableContainer,ResourceParameter... resourceParameters) throws Exception { return buildResourceMetaData(variableContainer, resourceParameters); } @Override public ContentMetaData getOutputMetaData(VariableContainer variableContainer,ResourceParameter... resourceParameters) throws Exception { return buildResourceMetaData(variableContainer, resourceParameters); } @Override protected void clearContent() throws Exception { // TODO Auto-generated method stub } @Override protected ContentMetaData buildResourceMetaData(VariableContainer variableContainer,ResourceParameter... resourceParameters) throws Exception { if(jcrResourceMetatData == null) { this.jcrResourceMetatData = new JcrContentMetaData(getResourceURI()); } return this.jcrResourceMetatData; } @Override public OutputStream getOutputStream(VariableContainer variableContainer,ResourceParameter... resourceParameters) throws Exception { //return the output stream we have, if it's already open if(this.pipedOutputStream != null) { return this.pipedOutputStream; } synchronized (isWriting) { isWriting = true; //If we're going to write. make sure we have a node to write to. if(getNode() == null) { performAction(variableContainer, Action.CREATE, resourceParameters); performAction(variableContainer, Action.COMMIT, resourceParameters); } final PipedInputStream pipedInputStream = new PipedInputStream(){ @Override public void close() throws IOException { //System.out.println("piped input close attempt: "+System.currentTimeMillis()); // TODO Auto-generated method stub super.close(); //System.out.println("piped input closed: "+System.currentTimeMillis()); } }; PipedOutputStream pipedOutputStream = new PipedOutputStream(pipedInputStream){ @Override public void close() throws IOException { //System.out.println("piped output close attempt: "+System.currentTimeMillis()); super.close(); //System.out.println("isWaiting = "+isWriting); Boolean _isWaiting = null; synchronized (isWriting) { _isWaiting = isWriting; } //don't let this close while we're still dealing with threaded output //System.out.println("isWaiting = "+_isWaiting); while(_isWaiting == true ) { try { // System.out.println("waiting for pipe thread to finish"); Thread.sleep(100); synchronized (isWriting) { _isWaiting = isWriting; } } catch (InterruptedException e) { e.printStackTrace(); } } //System.out.println("piped output closed: "+System.currentTimeMillis()); JcrResourceDescriptor.this.pipedOutputStream = null; } }; //MimeTable mt = MimeTable.getDefaultTable(); //String mimeType = mt.getContentTypeFor(file.getName()); //if (mimeType == null) mimeType = "application/octet-stream"; // Node fileNode = node.addNode("<name>", "nt:file"); //System.out.println( fileNode.getName() ); //final Node resNode = fileNode.addNode("jcr:content", "nt:resource"); //resNode.setProperty("jcr:mimeType", "<mimeType>"); //resNode.setProperty("jcr:encoding", ""); Runnable pipe = new Runnable() { @Override public void run() { try { MD5FilterInputStream md5FilterInputStream = new MD5FilterInputStream(pipedInputStream); ContentFormatTypeFilterInputStream contentFormatTypeFilterInputStream = new ContentFormatTypeFilterInputStream(md5FilterInputStream); MimeTypeFilterInputStream mimeTypeFilterInputStream = new MimeTypeFilterInputStream(contentFormatTypeFilterInputStream); SizeFilterInputStream sizeFilterInputStream = new SizeFilterInputStream(mimeTypeFilterInputStream); //System.out.println("pipe thread start read: "+System.currentTimeMillis()); synchronized (hasPipeThreadStarted) { hasPipeThreadStarted.notify(); } Binary binary = getNode().getSession().getValueFactory().createBinary( sizeFilterInputStream ); if(binary.getSize() != 0) { getNode().setProperty("jcr:data",binary); getNode().setProperty(contentFormatTypeFilterInputStream.getName(),contentFormatTypeFilterInputStream.getValue()); if(contentFormatTypeFilterInputStream.getContentFormatType() != ContentFormatType.BINARY) { byte[] buffer = new byte[(int) binary.getSize()]; binary.read(buffer, 0); getNode().setProperty("content",new String(buffer)); } else if(getNode().hasProperty("content")) { getNode().getProperty("content").remove(); } getNode().setProperty(sizeFilterInputStream.getName(),sizeFilterInputStream.getValue()); getNode().setProperty(mimeTypeFilterInputStream.getName(),mimeTypeFilterInputStream.getValue()); //getNode().setProperty("jcr:mimeType",mimeTypeFilterInputStream.getValue()); getNode().setProperty(md5FilterInputStream.getName(),md5FilterInputStream.getValue()); } else //there is no data { PropertyIterator propertyIterator = getNode().getProperties(); while(propertyIterator.hasNext()) { Property property = propertyIterator.nextProperty(); if(property.getName().startsWith("jcr:") == false) { property.remove(); } } if(getNode().hasProperty("jcr:data")) { getNode().getProperty("jcr:data").remove(); } } binary.dispose(); //System.out.println("pipe thread done: "+System.currentTimeMillis()); if(getNode().getSession() != null && getNode().getSession().isLive() && getNode().getSession().hasPendingChanges()) {//TODO check for autocommit setting getNode().getSession().save(); } } catch (Exception e) { e.printStackTrace(); } finally { //isWriting.notify(); synchronized (isWriting) { isWriting = false; } //cleanup session out of thread if(Thread.currentThread() instanceof ContextThread) { ((ContextThread) Thread.currentThread()).setSession(null); } } } }; synchronized (hasPipeThreadStarted) { //System.err.println( "PipeWrite T="+Thread.currentThread()+" S="+session+" N="+absPath); ContextThread contextThread = new ContextThread(pipe, getNode().getName()+" pipeThread-"+System.currentTimeMillis()); contextThread.setSession(getNode().getSession()); contextThread.start(); //Calendar lastModified = Calendar.getInstance(); //lastModified.setTimeInMillis(file.lastModified()); //resNode.setProperty("jcr:lastModified", lastModified); //Uti //System.out.println("waiting for pipe thread to start"); hasPipeThreadStarted.wait(1500); } this.pipedOutputStream = pipedOutputStream; return pipedOutputStream; } } @Override public InputStream getInputStream(VariableContainer variableContainer,ResourceParameter... resourceParameters) throws Exception { advanceState(State.STEPPING, variableContainer, resourceParameters); if(getNode().hasProperty("jcr:data")) { return getNode().getProperty("jcr:data").getBinary().getStream(); } else { return new ByteArrayInputStream(new byte[]{}); } } // @Override // public void writeXML(VariableContainer variableContainer, CElement element, ResourceParameter... resourceParameters) throws Exception // { // ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); // XPath.dumpNode(element, outputStream); // session.importXML(getResourceURI().getResourceURIString().replaceFirst("^repo:", ""), new ByteArrayInputStream(outputStream.toByteArray()), 0); // dump(session.getRootNode()); // session.save(); // String[] langs = session.getWorkspace().getQueryManager().getSupportedQueryLanguages(); // String[] prefixes = session.getNamespacePrefixes(); // // Query query = session.getWorkspace().getQueryManager().createQuery("SELECT * FROM [nt:unstructured] where NAME([nt:unstructured]) = 'server:log' order by message", "JCR-SQL2"); // QueryResult result = query.execute(); // // // // Iterate over the nodes in the results ... // // NodeIterator nodeIter = result.getNodes(); // System.out.println("============================="); // while ( nodeIter.hasNext() ) { // // Node _node = nodeIter.nextNode(); // System.out.println("===>"+_node.getName()+" type:"+_node.getPrimaryNodeType().getName()); // dump(_node); // // } // System.out.println("============================="); // session.logout(); // } @Override public boolean performAction(VariableContainer variableContainer,Action action,ResourceParameter... resourceParameters) throws Exception { super.addResourceParameters(variableContainer, resourceParameters); advanceState(State.OPEN, variableContainer, resourceParameters); boolean success = false; if (action == Action.CREATE) { if(CapoJcrServer.getSession().nodeExists(absPath) == false) { String[] relPath = absPath.split("/"); Node currentNode = CapoJcrServer.getSession().getRootNode(); for (String nodeName : relPath) { if(nodeName.isEmpty()) { currentNode = CapoJcrServer.getSession().getRootNode(); continue; } if(currentNode.hasNode(nodeName) == false) { currentNode.addNode(nodeName);//,"nt:folder"); if(currentNode.isNew()) { currentNode.setProperty("container", "true"); } } currentNode = currentNode.getNode(nodeName); } //session.getRootNode().addNode(getResourceURI().getResourceURIString().replaceFirst("^repo:/", "")); String containerFlag = getVarValue(variableContainer, DefaultParameters.CONTAINER); if (containerFlag != null && containerFlag.equalsIgnoreCase("true")) { getNode().setProperty("container", containerFlag); } refreshResourceMetaData(variableContainer, resourceParameters); success = true; } } else if (action == Action.DELETE) { if (getNode() != null) { Node node = getNode(); node.remove(); node.getSession().save(); refreshResourceMetaData(variableContainer, resourceParameters); success = true; } } else if (action == Action.COMMIT) { if(pipedOutputStream != null) { try { pipedOutputStream.close(); pipedOutputStream = null; } catch (Exception exception){} } if (getNode() != null) { getNode().getSession().save(); success = true; } } else if (action == Action.CHECKIN) { checkin(); refreshResourceMetaData(variableContainer, resourceParameters); success = true; } else if (action == Action.CHECKOUT) { checkout(); refreshResourceMetaData(variableContainer, resourceParameters); success = true; } else if (action == Action.RESTORE) { restore(ResourceParameterBuilder.getValue(Versionable.ResourceParameters.VERSION_UUID, resourceParameters)); refreshResourceMetaData(variableContainer, resourceParameters); success = true; } return success; } @Override protected Action[] getSupportedActions() { return (Action[]) EnumSet.allOf(Action.class).toArray(new Action[]{}); } /** Recursively outputs the contents of the given node. */ public static void dump(Node node) throws RepositoryException { // First output the node path if(node == null) { return; } System.out.println(node.getPath()); // Skip the virtual (and large!) jcr:system subtree // if (node.getPath().startsWith("/jcr:system")) { // return; // } // Then output the properties PropertyIterator properties = node.getProperties(); while (properties.hasNext()) { Property property = properties.nextProperty(); if(property.getName().equals("jcr:data") || property.getName().equals("content")) { continue; } if (property.getDefinition().isMultiple()) { // A multi-valued property, print all values Value[] values = property.getValues(); for (int i = 0; i < values.length; i++) { System.out.println( property.getPath() + " += " + values[i].getString()); } } else { // A single-valued property System.out.println( property.getPath() + " == " + property.getString()); } } // Finally output all the child nodes recursively NodeIterator nodes = node.getNodes(); while (nodes.hasNext()) { dump(nodes.nextNode()); } } @Override public void close(VariableContainer variableContainer, ResourceParameter... resourceParameters) throws Exception { super.close(variableContainer, resourceParameters); if(pipedOutputStream != null) { try { pipedOutputStream.close(); pipedOutputStream = null; } catch (Exception exception){} } if(CapoJcrServer.getSession() != null && CapoJcrServer.getSession().isLive() && CapoJcrServer.getSession().hasPendingChanges()) {//TODO check for autocommit setting CapoJcrServer.getSession().save(); } // if(isLocalSession == true && session != null && session.isLive()) // { // session.logout(); // ((JcrContentMetaData) getResourceMetaData(variableContainer, resourceParameters)).setNode(null); // // } // this.session = null; } // @Override // public ResourceDescriptor getChildResourceDescriptor(ControlElement callingControlElement, String relativeURI) throws Exception // { // ResourceDescriptor childResourceDescriptor = super.getChildResourceDescriptor(callingControlElement, relativeURI); // if(childResourceDescriptor instanceof JcrResourceDescriptor) // { // ((JcrResourceDescriptor) childResourceDescriptor).session = this.session; // } // return childResourceDescriptor; // } public void checkin() throws Exception { if (getNode() != null) { VersionManager versionManager = getNode().getSession().getWorkspace().getVersionManager(); Node node = getNode(); if(node.isCheckedOut() == false) //this should return true on anything that isn't versionable { //make sure that the node is versionable if(node.isNodeType("mix:versionable") == false) { try { //if this is not checked out AND ist' not versionable something is wrong, so try and check it out and see if that throws an exception versionManager.checkout(node.getPath()); } catch (Exception exception) { //if so we managed to get a node checked in without a versionable mixin //so let the addMixin call succeed no matter the cost by removing some validation from jack rabbit for the time being overrideJackrabbitValidation(node,ItemValidator.CHECK_CHECKED_OUT); } } } if(node.isNodeType("mix:versionable") == false) { node.addMixin("mix:versionable"); node.getSession().save(); if(originalItemValidator != null) { resetJackRabitValidation(node); } } versionManager.checkin(node.getPath()); } } /** * This should never really be used, but in testing a node was marked as checked in w/o the required mixin's for versioning. * So this is basically a method to override jackrabbit's validation methods so that we can still write to a checked in version. * @param n * @param optionsOverride * @throws Exception */ private void overrideJackrabbitValidation(Node n, int optionsOverride) throws Exception { try { NodeImpl node = (NodeImpl) n; SessionImpl session = (SessionImpl) node.getSession(); Field sessionContextField = SessionImpl.class.getDeclaredField("context"); sessionContextField.setAccessible(true); SessionContext sessionContext = (SessionContext) sessionContextField.get(session); Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); Field itemValidatorField = SessionContext.class.getDeclaredField("itemValidator"); itemValidatorField.setAccessible(true); originalItemValidator = (ItemValidator) itemValidatorField.get(sessionContext); modifiersField.setInt(itemValidatorField, itemValidatorField.getModifiers() & ~Modifier.FINAL); ItemValidator itemValidator = new ItemValidator(sessionContext){ @Override public synchronized boolean canModify(ItemImpl item, int options, int permissions) throws RepositoryException { options = options & ~optionsOverride; return super.canModify(item, options, permissions); } @Override public synchronized void checkModify(ItemImpl item, int options, int permissions) throws RepositoryException { options = options & ~optionsOverride; super.checkModify(item, options, permissions); } @Override public synchronized void checkRemove(ItemImpl item, int options, int permissions) throws RepositoryException { options = options & ~optionsOverride; super.checkRemove(item, options, permissions); } }; itemValidatorField.set(sessionContext, itemValidator); } catch (Exception e) { throw e; } } /** * this should restore any jackrabbit validation that was turned off. * @param n * @throws Exception */ private void resetJackRabitValidation(Node n) throws Exception { try { NodeImpl node = (NodeImpl) n; SessionImpl session = (SessionImpl) node.getSession(); Field sessionContextField = SessionImpl.class.getDeclaredField("context"); sessionContextField.setAccessible(true); SessionContext sessionContext = (SessionContext) sessionContextField.get(session); Field itemValidatorField = SessionContext.class.getDeclaredField("itemValidator"); itemValidatorField.setAccessible(true); itemValidatorField.set(sessionContext, originalItemValidator); Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(itemValidatorField, itemValidatorField.getModifiers() | Modifier.FINAL); modifiersField.setAccessible(false); itemValidatorField.setAccessible(false); sessionContextField.setAccessible(false); originalItemValidator = null; } catch (Exception e) { throw e; } } @Override public void checkout() throws Exception { if (getNode() != null) { VersionManager versionManager = getNode().getSession().getWorkspace().getVersionManager(); Node node = getNode(); getVersionHistory(); if(node.hasProperty("jcr:isCheckedOut") == true) { versionManager.checkout(node.getPath()); } } } @Override public boolean isVersioned() throws Exception { if (getNode() != null && getNode().hasProperty("jcr:isCheckedOut")) { return true; } return false; } @Override public List<ContentMetaData> getVersionHistory() throws Exception { Vector<ContentMetaData> versionHistory = new Vector<>(); if (getNode() != null && getNode().hasProperty("jcr:isCheckedOut")) { VersionManager versionManager = getNode().getSession().getWorkspace().getVersionManager(); VersionHistory history = versionManager.getVersionHistory(getNode().getPath()); boolean isRootVersion = true; for (VersionIterator it = history.getAllVersions(); it.hasNext();) { Version version = (Version) it.next(); version.getPath(); JcrContentMetaData contentMetaData = new JcrVersionContentMetaData(new ResourceURI("repo:"+getNode().getPath()+"?version="+version.getName())); //skip first version if(isRootVersion) { isRootVersion = false; continue; } versionHistory.add(contentMetaData); //dump(version.getFrozenNode()); //System.out.println(version.getName()); } } return versionHistory; } @Override public String getVersion(String versionUUID) throws Exception { VersionManager versionManager = getNode().getSession().getWorkspace().getVersionManager(); ResourceURI versionURI = new ResourceURI(versionUUID); VersionHistory history = versionManager.getVersionHistory(getNode().getPath()); Version version = history.getVersion(versionURI.getParameterMap().get("version")); if (version != null) { return "repo:"+version.getFrozenNode().getPath(); } return null; } @Override public void restore(String versionUUID) throws Exception { if (getNode() != null) { VersionManager versionManager = getNode().getSession().getWorkspace().getVersionManager(); ResourceURI versionURI = new ResourceURI(versionUUID); VersionHistory history = versionManager.getVersionHistory(getNode().getPath()); Version version = history.getVersion(versionURI.getParameterMap().get("version")); if (version != null) { versionManager.restore( version, true); } } } @Override public void remove(String versionUUID) throws Exception { if (getNode() != null) { if (getNode() != null) { VersionManager versionManager = getNode().getSession().getWorkspace().getVersionManager(); ResourceURI versionURI = new ResourceURI(versionUUID); VersionHistory history = versionManager.getVersionHistory(getNode().getPath()); Version version = history.getVersion(versionURI.getParameterMap().get("version")); if (version != null) { history.removeVersion(version.getName()); } } } } private Node getNode() throws Exception { if(CapoJcrServer.getSession().nodeExists(absPath)) { return CapoJcrServer.getSession().getNode(absPath); } else { return null; } } }